package com.usbserial.transaction;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Splits the incoming stream using a fixed magic header followed by a length byte.
 */
public class MagicHeaderLengthDecoder implements FrameDecoder<byte[]> {

    private final int[] headerPattern;
    private final int maxBufferLength;
    private final long clearTimeoutMillis;
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    private long lastDataMillis;

    public MagicHeaderLengthDecoder(int[] headerPattern, int maxBufferLength, long clearTimeoutMillis) {
        if (headerPattern == null || headerPattern.length == 0) {
            throw new IllegalArgumentException("Header must not be empty");
        }
        this.headerPattern = headerPattern.clone();
        this.maxBufferLength = Math.max(maxBufferLength, headerPattern.length * 2 + 1);
        this.clearTimeoutMillis = clearTimeoutMillis;
    }

    public MagicHeaderLengthDecoder(int[] headerPattern) {
        this(headerPattern, 1024, 1000L);
    }

    @Override
    public synchronized List<byte[]> decode(byte[] chunk) {
        if (chunk == null || chunk.length == 0) {
            checkTimeout();
            return Collections.emptyList();
        }
        buffer.write(chunk, 0, chunk.length);
        lastDataMillis = System.currentTimeMillis();
        truncateIfNeeded();

        byte[] raw = buffer.toByteArray();
        List<byte[]> frames = new ArrayList<>();
        int index = indexOfPattern(raw, 0);
        while (index >= 0) {
            if (raw.length - index < headerPattern.length + 1) {
                break;
            }
            int payloadLength = raw[index + headerPattern.length] & 0xFF;
            int frameEnd = index + headerPattern.length + 1 + payloadLength;
            if (raw.length < frameEnd) {
                break;
            }
            frames.add(Arrays.copyOfRange(raw, index, frameEnd));
            raw = Arrays.copyOfRange(raw, frameEnd, raw.length);
            index = indexOfPattern(raw, 0);
        }
        buffer.reset();
        buffer.write(raw, 0, raw.length);
        return frames;
    }

    private void checkTimeout() {
        if (clearTimeoutMillis <= 0) {
            return;
        }
        long now = System.currentTimeMillis();
        if (now - lastDataMillis > clearTimeoutMillis) {
            buffer.reset();
        }
    }

    private int indexOfPattern(byte[] raw, int start) {
        if (raw.length - start < headerPattern.length) {
            return -1;
        }
        outer: for (int i = start; i <= raw.length - headerPattern.length; i++) {
            for (int j = 0; j < headerPattern.length; j++) {
                int expected = headerPattern[j];
                if (expected >= 0 && (raw[i + j] & 0xFF) != expected) {
                    continue outer;
                }
            }
            return i;
        }
        return -1;
    }

    private void truncateIfNeeded() {
        byte[] raw = buffer.toByteArray();
        if (raw.length <= maxBufferLength) {
            return;
        }
        byte[] trimmed = Arrays.copyOfRange(raw, raw.length - maxBufferLength, raw.length);
        buffer.reset();
        buffer.write(trimmed, 0, trimmed.length);
    }

    @Override
    public synchronized void reset() {
        buffer.reset();
    }
}
